package com.netx.generics.util;
import com.netx.generics.basic.Checker;
import com.netx.generics.basic.Logger;
import com.netx.generics.basic.AbstractLogger;
import com.netx.generics.basic.DefaultLogger;
import com.netx.generics.time.Moment;
import com.netx.generics.time.TimeValue;
import com.netx.generics.time.TimeValue.MEASURE;
import com.netx.generics.io.BufferedWriter;
import com.netx.generics.io.Directory;
import com.netx.generics.io.File;
import com.netx.generics.io.FilenameFilter;
import com.netx.generics.io.SearchOptions;
import com.netx.generics.util.Arrays;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;


public class CsvLogger extends AbstractLogger {

	private final String[] _line;
	private final String[] _header;
	private final String _logFilename;
	private final String _errorFilename;
	private final Directory _dir;
	private final DateFormat _df;
	private int _messageIndex = -1;
	private int _idIndex = -1;
	private int _typeIndex = -1;
	private int _dateIndex = -1;
	private int _timeIndex = -1;
	private int _traceIndex = -1;
	private Logger _default;
	
	public CsvLogger(String[] params, Directory dir, String filenameFormat) throws IOException {
		this(params, dir, filenameFormat, 1);
	}

	public CsvLogger(String[] params, Directory dir, String filenameFormat, int numDays) throws IOException {
		Checker.checkEmpty(params, "params");
		Checker.checkNull(dir, "dir");
		Checker.checkEmpty(filenameFormat, "filenameFormat");
		Checker.checkMinValue(numDays, 1, "numDays");
		// identify params:
		for(int i=0; i<params.length; i++) {
			if(params[i].equalsIgnoreCase("message")) {
				_messageIndex = i;
			}
			else if(params[i].equalsIgnoreCase("id")) {
				_idIndex = i;
			}
			else if(params[i].equalsIgnoreCase("type")) {
				_typeIndex = i;
			}
			else if(params[i].toLowerCase().startsWith("date")) {
				_dateIndex = i;
			}
			else if(params[i].equalsIgnoreCase("time")) {
				_timeIndex = i;
			}
			else if(params[i].equalsIgnoreCase("trace")) {
				_traceIndex = i;
			}
			else {
				throw new IllegalArgumentException("illegal parameter: "+params[i]);
			}
		}
		if(_messageIndex == -1) {
			throw new IllegalArgumentException("'Message' column is not specified");
		}
		// get appropriate DateFormat:
		// (if not set it will default to yyyy-MM-dd, to avoid the English format comma)
		DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
		String dateColumnName = null;
		if(_dateIndex != -1) {
			dateColumnName = params[_dateIndex];
			if(_dateIndex != -1 && params[_dateIndex].length() != 4) {
				dateColumnName = params[_dateIndex].substring(0, 4);
				// extract DateFormat argument:
				String dateParam = params[_dateIndex].toLowerCase();
				if(!dateParam.startsWith("date(") || dateParam.charAt(dateParam.length()-1) != ')') {
					throw new IllegalArgumentException("invalid format for DATE special column: "+dateParam);
				}
				// directly from 'params' to keep case:
				String arg = params[_dateIndex].substring("date(".length(), dateParam.length()-1);
				if(arg.charAt(0) != '"' && arg.charAt(0) != '\'') {
					throw new IllegalArgumentException("DATE argument must be enclosed in \" or ': "+dateParam);
				}
				if(arg.charAt(0) != arg.charAt(arg.length()-1)) {
					throw new IllegalArgumentException("DATE argument must be enclosed in "+arg.charAt(0)+": "+dateParam);
				}
				arg = arg.substring(1, arg.length()-1);
				try {
					df = new SimpleDateFormat(arg);
				}
				catch(IllegalArgumentException iae) {
					throw new IllegalArgumentException("invalid DATE argument: "+iae.getMessage());
				}
			}
		}
		// init vars:
		_df = df;
		_line = new String[params.length];
		_dir = dir;
		_header = new String[params.length];
		Arrays.copy(params, _header);
		if(_dateIndex != -1) {
			_header[_dateIndex] = dateColumnName;
		}
		// determine concrete filename:
		if(numDays > 1) {
			// check filenameFormat:
			Builtins.evaluate(filenameFormat);
			if(!filenameFormat.contains("$now") && !filenameFormat.contains("$date")) {
				throw new IllegalArgumentException("$now or $date built-ins not found in filenameFormat");
			}
			int countNows  = Strings.countOccurrences(filenameFormat, "$now");
			int countDates = Strings.countOccurrences(filenameFormat, "$date");
			if(countNows>1) {
				throw new IllegalArgumentException("$now built-in was found "+countNows+" times");
			}
			if(countDates>1) {
				throw new IllegalArgumentException("$date built-in was found "+countDates+" times");
			}
			if(countDates + countNows > 1) {
				throw new IllegalArgumentException("$now and $date built-ins used in conjunction");
			}
			String builtin = countNows==1 ? "$now" : "$date";

			// get pre-builtin expression:
			int beginIndex = filenameFormat.indexOf(builtin);
			String preExpr = Builtins.evaluate(filenameFormat.substring(0, beginIndex));
			// get builtin argument:
			beginIndex += builtin.length()+1;
			int endIndex = beginIndex;
			for(int countSeparators = 1; countSeparators > 0; endIndex++) {
				if(filenameFormat.charAt(endIndex)=='(') {
					countSeparators++;
				}
				else if(filenameFormat.charAt(endIndex)==')') {
					countSeparators--;
				}
			}
			String arg = Builtins.evaluate(filenameFormat.substring(beginIndex, endIndex-1));
			// Remove leading and trailing " or ':
			if(arg.charAt(0)=='"' || arg.charAt(0)=='\'') {
				arg = arg.substring(1, arg.length()-1);
			}
			// get post-builtin expression:
			String postExpr = Builtins.evaluate(filenameFormat.substring(endIndex));

			// get time of the last file:
			String[] list = _dir.listFiles(new SearchOptions(
				new LogFilenameFilter(preExpr, postExpr),
				true,
				SearchOptions.ORDER.NAME_ASCENDING
			));
			boolean needsNewFiles = false;
			if(list.length > 0) {
				// check need to create new file:
				String filename = list[list.length-1];
				String dateExpr = filename.substring(preExpr.length(), preExpr.length()+arg.length());
				Moment m = Moment.valueOf(dateExpr, new SimpleDateFormat(arg));
				long fileDays = TimeValue.convert(m.getTimeInMilliseconds(), MEASURE.MILLISECONDS, MEASURE.DAYS);
				long nowDays = TimeValue.convert(new Moment().getTimeInMilliseconds(), MEASURE.MILLISECONDS, MEASURE.DAYS);
				if(nowDays-fileDays > numDays) {
					needsNewFiles = true;
				}
			}
			else {
				needsNewFiles = true;
			}
			
			// choose target filename:
			if(needsNewFiles) {
				_logFilename = Builtins.evaluate(filenameFormat+".csv");
				_errorFilename = Builtins.evaluate(filenameFormat+".txt");
			}
			else {
				_logFilename = list[list.length-1];
				_errorFilename = _logFilename.substring(0, _logFilename.length()-".csv".length())+".txt";
			}
		}
		else {
			_logFilename = Builtins.evaluate(filenameFormat+".csv");
			_errorFilename = Builtins.evaluate(filenameFormat+".txt");
		}
		_default = null;
	}

	protected synchronized void output(StackTraceElement place, LEVEL type, String message, Throwable t) throws IOException {
		// create line:
		Moment m = new Moment();
		if(message == null) {
			if(t != null) {
				_line[_messageIndex] = t.toString();
			}
			else {
				_line[_messageIndex] = null;
			}
		}
		else {
			_line[_messageIndex] = message;
		}
		if(_idIndex != -1) {
			_line[_idIndex] = m.getTimeInMilliseconds()+"";
		}
		if(_typeIndex != -1) {
			_line[_typeIndex] = type.toString();
		}
		if(_dateIndex != -1) {
			_line[_dateIndex] = m.getDate().format(_df);
		}
		if(_timeIndex != -1) {
			_line[_timeIndex] = m.getTime().format();
		}
		if(_traceIndex != -1) {
			_line[_traceIndex] = place.toString();
		}
		// perform log output:
		boolean foundFile = true;
		File log = _dir.getFile(_logFilename);
		if(log == null) {
			foundFile = false;
			log = _dir.createFile(_logFilename);
		}
		PrintWriter logWriter = new PrintWriter(new BufferedWriter(log.getOutputStream(true)));
		if(!foundFile) {
			logWriter.println(_getLine(_header));
		}
		logWriter.println(_getLine(_line));
		logWriter.close();
		// perform error output:
		if(t != null) {
			File errorFile = _dir.getFile(_errorFilename, true);
			PrintWriter errorWriter = new PrintWriter(new BufferedWriter(errorFile.getOutputStream(true)));
			errorWriter.print("ID: ");
			errorWriter.println(m.getTimeInMilliseconds());
			t.printStackTrace(errorWriter);
			errorWriter.println();
			errorWriter.close();
		}
		// Log to stdout, if necessary:
		if(_default != null) {
			if(type == LEVEL.INFO) {
				_default.info(message);
			}
			else if(type == LEVEL.WARNING) {
				if(t == null) {
					_default.warning(message);
				}
				else {
					_default.warning(message, t);
				}
			}
			else if(type == LEVEL.ERROR) {
				if(t == null) {
					_default.error(message, null);
				}
				else {
					_default.error(t);
				}
			}
		}
	}
	
	public boolean getEchoToStdout() {
		return _default != null;
	}
	
	public void setEchoToStdout(boolean echoToStdout) {
		if(echoToStdout) {
			_default = new DefaultLogger();
		}
		else {
			_default = null;
		}
	}

	private String _getLine(String[] array) {
		StringBuilder sb = new StringBuilder();
		for(int i=0; i<array.length; i++) {
			sb.append(array[i]);
			if(i < array.length-1) {
				sb.append(",");
			}
		}
		return sb.toString();
	}
	
	private static class LogFilenameFilter implements FilenameFilter {
		private final String _prefix;
		private final String _suffix;
		
		public LogFilenameFilter(String prefix, String suffix) {
			_prefix = prefix;
			_suffix = suffix+".csv";
		}
		
		public boolean accept(String name) {
			return name.startsWith(_prefix) && name.endsWith(_suffix);
		}
	}
}
